Лекция посвящена ООП-механизмам C++ в контексте сетевых приложений.
Студенты уже знакомы с базовыми конструкциями (лекция 03), теперь
переходим к организации классов, наследованию и полиморфизму.
Акцент: современные практики (Rule of Zero, override, smart pointers).
Порядок тем: от структуры класса к наследованию, затем полиморфизм.
На лекцию ~90 минут. Время на вопросы — в конце.
struct vs class: единственное отличие — default access (public у struct,
private у class). Семантически: struct — данные без поведения,
class — инкапсуляция данных и методов.
Рекомендация: все поля данных — private, доступ — через геттеры/сеттеры.
protected используется осторожно — лучше через private + protected-методы.
friend нарушает инкапсуляцию — используйте только для тестов и
operator<<. friend не наследуется и не транзитивен.
Практика: делайте данные private, методы — public/protected.
Золотое правило: минимальная видимость — лучший выбор.
Геттеры должны быть const — не изменяют объект.
Возврат const& для строк избегает копирования.
Сеттеры валидируют — инкапсуляция защищает данные.
MTU (Maximum Transmission Unit) = 1500 байт для Ethernet —
реальное ограничение в сети, используемое в примере.
= delete — явно запрещаем копирование. Соединение нельзя «размножить»:
два объекта с одним сокетом/ключом — ошибка.
Используйте = delete для запрета нежелательных операций.
Альтернатива: наследование от boost::noncopyable.
Список инициализации (: member(value)) — ЕДИНСТВЕННЫЙ способ
инициализировать const-поля, ссылки и поля без конструктора по умолчанию.
Порядок инициализации — порядок объявления в классе, НЕ порядок
в списке инициализации. Компилятор выдаёт warning если порядок не совпадает.
move-конструктор «крадёт» ресурсы у исходного объекта — важен для
std::unique_ptr, std::string, std::vector.
noexcept — обещание не бросать исключения, позволяет оптимизации.
Делегирующий конструктор — вызывает другой конструктор того же класса.
Избегает дублирования кода инициализации.
Внимание: при делегировании тело делегирующего конструктора выполняется
ПОСЛЕ вызванного конструктора. Нельзя одновременно делегировать и
инициализировать поля в списке.
Rule of Five: если класс владеет ресурсом (сокет, файл, память),
определите все 5: деструктор, copy ctor, copy assign, move ctor, move assign.
В данном примере копирование запрещено (= delete), т.к. дублировать
сокет нельзя. Перемещение разрешено — передаём владение.
В идеале — оберните сокет в RAII-класс и используйте Rule of Zero.
Rule of Zero: если класс не владеет raw-ресурсами — не определяйте
никаких специальных функций. Компилятор сгенерирует правильные.
Rule of Five: если владеете (raw pointer, fd, HANDLE) — определите
все пять. Нарушение этого правила — источник багов.
Всегда стремитесь к Rule of Zero через RAII-обёртки.
explicit предотвращает неявные преобразования: SocketGuard s = 5; — ошибка.
Стек — всегда предпочтительнее: нет аллокации, нет утечек, лучше
кэш-локальность.
make_unique — exception-safe, короче, единый стиль.
emplace_back vs push_back: emplace_back конструирует объект прямо
в контейнере, без промежуточного копирования/перемещения.
RAII = Resource Acquisition Is Initialization.
Ресурс захватывается в конструкторе, освобождается в деструкторе.
Гарантия: деструктор вызовется ДАЖЕ при исключении (stack unwinding).
Это главное преимущество C++ перед Java/C# (нет finally — он не нужен).
Перегрузка (overloading) — несколько функций с ОДИНАКОВЫМ именем,
но РАЗНЫМИ сигнатурами (типы/количество параметров).
Возвращаемый тип НЕ участвует в разрешении перегрузки!
const на методе — часть сигнатуры: const-объект вызывает const-версию.
Нельзя перегрузить по умолчательным параметрам — это другая функция.
Перегружаемые операторы: +, -, *, /, ==, !=, <, >, <<, >>, [], (), ->
НЕ перегружаемые: ., .*, ::, ?:, sizeof, typeid
Правило: перегружайте операторы только если смысл очевиден.
operator< нужен для использования в std::map, std::set, std::sort.
В C++20 можно заменить все сравнения одним operator<=>
override — КРИТИЧЕСКИ важный атрибут. Без него опечатка в сигнатуре
создаст НОВЫЙ метод вместо переопределения — баг без ошибки компиляции.
final на классе: нельзя наследовать (class MyUDP : public UDPProtocol — ошибка).
final на методе: нельзя переопределить в дальнейших наследниках.
Практика: ВСЕГДА используйте override. Включите -Wsuggest-override в GCC.
Виртуальный деструктор — ОБЯЗАТЕЛЕН для полиморфных базовых классов!
Без него: Base* ptr = new Derived(); delete ptr; — UB (деструктор
Derived не вызовется, ресурсы утечкут).
Чисто виртуальный (= 0) — метод без реализации, класс нельзя создать.
Вызов базового метода: Connection::establish() — частый паттерн
для расширения, а не замены поведения.
Порядок конструирования: Device → NetworkDevice → Router.
Порядок уничтожения: Router → NetworkDevice → Device (обратный).
Наследование — отношение «IS-A»: Router IS-A NetworkDevice IS-A Device.
Если отношения IS-A нет — используйте композицию (HAS-A).
final на Router — запрещает дальнейшее наследование.
public наследование — подавляющее большинство случаев (99%).
private наследование — альтернатива композиции, когда нужен доступ
к protected-членам базы или override виртуальных методов.
protected наследование — почти никогда не используется.
В Qt:几乎所有 классы используют public наследование от QObject.
Рекомендация: если сомневаетесь — используйте public + композиция.
Множественное наследование интерфейсов (все методы = 0) — безопасно,
нет проблемы ромба. Это рекомендуемый паттерн в C++.
Множественное наследование КОНКРЕТНЫХ классов — опасно:
проблема ромбовидного наследования, дублирование данных.
Java/C# решают это через интерфейсы (один класс + много интерфейсов).
В C++ «интерфейс» = класс только с чисто виртуальными методами.
Без virtual: Transceiver содержит ДВА экземпляра NetworkComponent —
один через Sender, другой через Receiver. componentId неоднозначен!
virtual наследование: один общий экземпляр NetworkComponent.
Цена: дополнительный указатель (vptr) в каждом виртуальном базовом классе.
Внимание: самый производный класс (Transceiver) ВСЕГДА вызывает
конструктор виртуальной базы (NetworkComponent) напрямую.
Рекомендация: избегайте ромбовидного наследования через интерфейсы.
Сокрытие (hiding): если наследник объявляет метод с тем же именем,
ВСЕ перегрузки базового класса скрываются. Решение:
using BaseServer::start; // в классе наследника — раскрывает все overload-ы
Не путайте hiding и overriding:
hiding — метод НЕ virtual, вызов определяется ТИПОМ указателя.
overriding — метод virtual, вызов определяется ОБЪЕКТОМ (runtime).
vtable (virtual table) — скрытая таблица указателей на функции.
Каждый объект с virtual-методами содержит vptr → указатель на vtable.
Вызов virtual-метода: obj->vptr[method_index](args) — косвенный вызов.
Overhead: одно дополнительное чтение из памяти (обычно незначительно).
devirtualization: компилятор может оптимизировать, если тип известен
на этапе компиляции. final помогает компилятору девиртуализировать.
Абстрактный класс = хотя бы один чисто виртуальный метод (= 0).
Нельзя создать объект: Protocol p; — ошибка компиляции.
Можно иметь указатель/ссылку: Protocol* p = new HTTPProtocol();
Обычные методы в абстрактном классе — общая логика (DRY-принцип).
Чисто виртуальный метод МОЖЕТ иметь реализацию:
virtual void foo() = 0; // в .h
void Protocol::foo() { /* default impl */ } // в .cpp
Наследник вызывает: Protocol::foo();
C++ не имеет ключевого слова interface (как Java/C#).
Конвенция: имя с префиксом I (IEncryption), только чисто виртуальные
методы + virtual деструктор. Это «чистый интерфейс».
Преимущества:
- Разделение «что делать» от «как делать»
- Легко подменять реализацию (mock-объекты для тестов)
- Dependency Injection через указатель на интерфейс
В Qt: аналогичные паттерны используются в QIODevice, QAbstractSocket.
Шаблоны — обобщённое программирование. Код генерируется для каждого
типа при компиляции (code bloat — потенциальная проблема).
explicit на конструкторе предотвращает: NetworkBuffer buf = 10;
std::optional<T> — безопасная альтернатива T() при пустом буфере.
Позволяет вызывающей стороне проверить: if (auto v = buf.get()) { ... }
В Qt: шаблоны используются реже, предпочитаются Qt-контейнеры.
Четыре столпа ООП, применённые к сетевым приложениям:
- Инкапсуляция: защита ключей шифрования, соединений
- Наследование: иерархия Device → NetworkDevice → Router
- Полиморфизм: единый handler для TCP/UDP/HTTP
- Абстракция: Protocol как база для всех протоколов
Главный принцип: пишите безопасный код. Компилятор — ваш друг,
используйте override, = delete, final, explicit, const, noexcept
для выражения намерений и раннего обнаружения ошибок.
Вопросы обновлены: добавлен вопрос о Rule of Zero/Three/Five (3),
уточнён вопрос про override (5). Ответы:
3) Zero: класс без ресурсов — не определять спецфункции. Five: владеет
ресурсом — определить все пять.
5) Без override опечатка в сигнатуре создаст новый метод — баг без
ошибки компиляции. override превращает это в ошибку компиляции.